/*-------------------------------------------------------------------------
 *
 * nodeHashjoin.c
 *	  处理哈希连接节点的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/executor/nodeHashjoin.c
 *
 * PARALLELISM
 *
 * 哈希连接可以通过几种方式参与并行查询执行。并行无知的哈希连接是指节点不知道它是并行计划的一部分。在这种情况下，每个后端使用内部计划的副本来构建哈希表副本，外部计划可以是从部分或完整路径构建，这样哈希连接的结果将相应地是部分或完整。并行感知的哈希连接表现不同，在后端之间协调工作，并在 EXPLAIN 输出中表示为“Parallel Hash Join”。并行哈希连接总是与并行哈希节点一起出现。
 *
 * 并行感知的哈希连接使用相同的每个后端状态机来跟踪哈希连接算法的进度，类似于并行无知的哈希连接。在并行感知的哈希连接中，还有一个共享状态机，协作的后端用来同步它们的本地状态机和程序计数器。共享状态机使用 Barrier IPC 原语进行管理。当所有附加的参与者到达障碍时，阶段向前推进，所有等待的参与者被释放。
 *
 * 当参与者开始在并行哈希连接中工作时，必须首先弄清楚已经取得了多少进展，因为参与者不会互相等待以开始。因此，在代码中有关键点的 switch 语句，我们必须将本地状态机与阶段同步，然后跳转到算法的正确部分以便开始。
 *
 * 一个叫做 build_barrier 的障碍用于协调整个哈希阶段。阶段由一个整数表示，该整数从零开始并逐个增加，但在代码中，它被以下符号名称引用：
 *
 *   PHJ_BUILD_ELECTING              -- 初始状态
 *   PHJ_BUILD_ALLOCATING            -- 一个人设置批次和表 0
 *   PHJ_BUILD_HASHING_INNER         -- 全部哈希内部关系
 *   PHJ_BUILD_HASHING_OUTER         -- （多批次仅适用）所有哈希外部关系
 *   PHJ_BUILD_RUNNING               -- 构建完成，可以开始探测
 *   PHJ_BUILD_DONE                  -- 所有工作完成，一人释放批次
 *
 * 在阶段 PHJ_BUILD_HASHING_INNER 中，可能会重复使用一对单独的障碍以协调批次或桶数量的扩展。它们的阶段如下：
 *
 *   PHJ_GROW_BATCHES_ELECTING       -- 初始状态
 *   PHJ_GROW_BATCHES_ALLOCATING     -- 一个人分配新的批次
 *   PHJ_GROW_BATCHES_REPARTITIONING -- 全部重新分区
 *   PHJ_GROW_BATCHES_FINISHING      -- 一人清理，检测偏斜
 *
 *   PHJ_GROW_BUCKETS_ELECTING       -- 初始状态
 *   PHJ_GROW_BUCKETS_ALLOCATING     -- 一个人分配新的桶
 *   PHJ_GROW_BUCKETS_REINSERTING    -- 全部插入元组
 *
 * 如果规划者正确估算了批次和桶的数量，则不需要这些操作，然而，另一方面，在散列内部关系时，我们可能需要多次扩展桶或批次以保持在内存预算和负载因子目标范围内。出于这个原因，它是一对使用循环阶段的独立障碍。
 *
 * 阶段 PHJ_BUILD_HASHING_OUTER 仅在多批次连接中必需，因为我们需要在前面将外部关系分割成批次，以能够完全独立地处理批次。相比之下，并行无知的算法在扫描和探测时遇到元组时直接将其“向前”丢向“后续”批次，因为它能够按照串行顺序处理批次。
 *
 * 一旦达到 PHJ_BUILD_RUNNING，后端就会分开处理不同的批次，或者如果没有足够的批次，则一起合作探测批次。每个批次都有一个独立的障碍，具有以下阶段：
 *
 *  PHJ_BATCH_ELECTING       -- 初始状态
 *  PHJ_BATCH_ALLOCATING     -- 一个人分配桶
 *  PHJ_BATCH_LOADING        -- 全部从磁盘加载哈希表
 *  PHJ_BATCH_PROBING        -- 全部进行探测
 *  PHJ_BATCH_DONE           -- 结束
 *
 * 批次 0 是一个特例，因为它开始于阶段 PHJ_BATCH_PROBING；批次 0 的哈希表将在阶段 PHJ_BUILD_HASHING_INNER 中填充，因此我们可以跳过加载。
 *
 * 最初，我们尝试通过所有参与者的 hash_mem 创建一个大型共享哈希表来计划单批哈希连接。如果在计划或执行时间证明这是不可能的，我们将回退到常规大小的 hash_mem 哈希表。
 *
 * 为了避免死锁，我们绝不会等待任何障碍，除非知道所有其他附加的后端正在积极执行该节点或已经完成。实际上，这意味着我们绝不会在附加到障碍时发出元组，除非障碍已达到不再有进程会再等待的阶段。我们在阶段 PHJ_BUILD_RUNNING 中附加到构建障碍时发出元组，在阶段 PHJ_BATCH_PROBING 中附加到每个批次的障碍时发出元组。它们分别提前推进到 PHJ_BUILD_DONE 和 PHJ_BATCH_DONE，而不等待，使用 BarrierArriveAndDetach()。最后一个脱离的接收不同的返回值，以便它知道可以安全清理。任何在达成该阶段后才附加的滞后进程会发现参与或访问相关共享内存对象已经太晚。
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/htup_details.h"
#include "access/parallel.h"
#include "executor/executor.h"
#include "executor/hashjoin.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "utils/memutils.h"
#include "utils/sharedtuplestore.h"


/*
 * ExecHashJoin状态机的状态
 */
#define HJ_BUILD_HASHTABLE		1
#define HJ_NEED_NEW_OUTER		2
#define HJ_SCAN_BUCKET			3
#define HJ_FILL_OUTER_TUPLE		4
#define HJ_FILL_INNER_TUPLES	5
#define HJ_NEED_NEW_BATCH		6

/* 如果在外部关系上进行null填充，则返回true */
#define HJ_FILL_OUTER(hjstate)	((hjstate)->hj_NullInnerTupleSlot != NULL)
/* 如果在内部关系上进行null填充，则返回true */
#define HJ_FILL_INNER(hjstate)	((hjstate)->hj_NullOuterTupleSlot != NULL)

static TupleTableSlot *fc_ExecHashJoinOuterGetTuple(PlanState *fc_outerNode,
												 HashJoinState *fc_hjstate,
												 uint32 *fc_hashvalue);
static TupleTableSlot *fc_ExecParallelHashJoinOuterGetTuple(PlanState *fc_outerNode,
														 HashJoinState *fc_hjstate,
														 uint32 *fc_hashvalue);
static TupleTableSlot *fc_ExecHashJoinGetSavedTuple(HashJoinState *fc_hjstate,
												 BufFile *fc_file,
												 uint32 *fc_hashvalue,
												 TupleTableSlot *fc_tupleSlot);
static bool fc_ExecHashJoinNewBatch(HashJoinState *fc_hjstate);
static bool fc_ExecParallelHashJoinNewBatch(HashJoinState *fc_hjstate);
static void fc_ExecParallelHashJoinPartitionOuter(HashJoinState *fc_node);


/* ----------------------------------------------------------------
 *		ExecHashJoinImpl
 *
 *		此函数实现Hybrid Hashjoin算法。它标记为始终内联属性，以便ExecHashJoin()和ExecParallelHashJoin()可以内联它。尊重该属性的编译器应该创建专为parallel == true 和parallel == false进行的版本，去除不必要的分支。
 *
 *		注意：我们在其上构建哈希表的关系是“内部的”，而另一个是“外部的”。
 * ----------------------------------------------------------------
 */
static pg_attribute_always_inline TupleTableSlot *
fc_ExecHashJoinImpl(PlanState *fc_pstate, bool fc_parallel)
{
	HashJoinState *fc_node = castNode(HashJoinState, fc_pstate);
	PlanState  *fc_outerNode;
	HashState  *fc_hashNode;
	ExprState  *fc_joinqual;
	ExprState  *fc_otherqual;
	ExprContext *fc_econtext;
	HashJoinTable hashtable;
	TupleTableSlot *fc_outerTupleSlot;
	uint32		fc_hashvalue;
	int			fc_batchno;
	ParallelHashJoinState *fc_parallel_state;

	/*
	 * 从HashJoin节点获取信息
	 */
	fc_joinqual = fc_node->js.joinqual;
	fc_otherqual = fc_node->js.ps.qual;
	fc_hashNode = (HashState *) innerPlanState(fc_node);
	fc_outerNode = outerPlanState(fc_node);
	hashtable = fc_node->hj_HashTable;
	fc_econtext = fc_node->js.ps.ps_ExprContext;
	fc_parallel_state = fc_hashNode->parallel_state;

	/*
	 * 重置每个元组的内存上下文，以释放在上一个元组周期中分配的任何表达式评估
	 * 存储。
	 */
	ResetExprContext(fc_econtext);

	/*
	 * 运行哈希连接状态机
	 */
	for (;;)
	{
		/*
		 * 在返回元组之前，可能会多次迭代此循环，在某些病态情况下，例如需要将当前批次的大部分内容移动到后续批次。因此，让我们每次都检查中断。
		 */
		CHECK_FOR_INTERRUPTS();

		switch (fc_node->hj_JoinState)
		{
			case HJ_BUILD_HASHTABLE:

				/*
				 * 第一次遍历：为内部关系构建哈希表。
				 */
				Assert(hashtable == NULL);

				/*
				 * 如果外部关系完全为空，并且这不是右/full连接，我们可以在不构建哈希表的情况下退出。但是，对于内部连接，只有当外部关系的启动成本低于构建哈希表的预计成本时，检查此情况才有意义。否则，最好先构建哈希表，然后查看内部关系是否为空。（当它是左连接时，我们应该始终进行此检查，因为我们无论如何都无法因为内部关系为空而跳过连接。）
				 *
				 * 如果我们正在重新扫描连接，我们会利用先前扫描获得的信息：如果先前扫描发现外部关系非空，则不要尝试预取。尽管这并不是100%可靠，因为在新参数下外部关系可能会产生不同的结果，但这是一个好的启发式。
				 *
				 * 唯一的检查方法是尝试从外部计划节点获取元组。如果成功，我们必须将其存储以供ExecHashJoinOuterGetTuple后续使用。
				 */
				if (HJ_FILL_INNER(fc_node))
				{
					/* 没有机会不构建哈希表 */
					fc_node->hj_FirstOuterTupleSlot = NULL;
				}
				else if (fc_parallel)
				{
					
/*
					 * 空外部优化未针对共享哈希表实现，因为没有任何参与者可以确定没有外部元组，并且尚不清楚是否值得为达成共识而付出同步开销。因此我们必须构建哈希表。
					 */
					fc_node->hj_FirstOuterTupleSlot = NULL;
				}
				else if (HJ_FILL_OUTER(fc_node) ||
						 (fc_outerNode->plan->startup_cost < fc_hashNode->ps.plan->total_cost &&
						  !fc_node->hj_OuterNotEmpty))
				{
					fc_node->hj_FirstOuterTupleSlot = ExecProcNode(fc_outerNode);
					if (TupIsNull(fc_node->hj_FirstOuterTupleSlot))
					{
						fc_node->hj_OuterNotEmpty = false;
						return NULL;
					}
					else
						fc_node->hj_OuterNotEmpty = true;
				}
				else
					fc_node->hj_FirstOuterTupleSlot = NULL;

				/*
				 * 创建哈希表。如果使用并行哈希，则首先到达这里的人将创建哈希表，任何后来的到达者将只是附加到它上面。
				 */
				hashtable = ExecHashTableCreate(fc_hashNode,
												fc_node->hj_HashOperators,
												fc_node->hj_Collations,
												HJ_FILL_INNER(fc_node));
				fc_node->hj_HashTable = hashtable;

				/*
				 * 执行哈希节点以构建哈希表。如果使用并行哈希，则我们将尝试帮助哈希，除非我们到达得太晚。
				 */
				fc_hashNode->hashtable = hashtable;
				(void) MultiExecProcNode((PlanState *) fc_hashNode);

				/*
				 * 如果内部关系完全为空，并且我们没有进行左外连接，则可以在不扫描外部关系的情况下退出。
				 */
				if (hashtable->totalTuples == 0 && !HJ_FILL_OUTER(fc_node))
				{
					if (fc_parallel)
					{
						/*
						 * 在继续之前，将构建障碍推进到 PHJ_BUILD_RUNNING，以便我们可以协商资源清理。
						 */
						Barrier    *fc_build_barrier = &fc_parallel_state->build_barrier;

						while (BarrierPhase(fc_build_barrier) < PHJ_BUILD_RUNNING)
							BarrierArriveAndWait(fc_build_barrier, 0);
					}
					return NULL;
				}

				/*
				 * 需要记住自我们开始扫描外部关系以来 nbatch 是否增加了。
				 */
				hashtable->nbatch_outstart = hashtable->nbatch;

				/*
				 * 重置 OuterNotEmpty 以进行扫描。（如果我们上面获取了一个元组也没关系，因为 ExecHashJoinOuterGetTuple 将立即重新设置它。）
				 */
				fc_node->hj_OuterNotEmpty = false;

				if (fc_parallel)
				{
					Barrier    *fc_build_barrier;

					fc_build_barrier = &fc_parallel_state->build_barrier;
					Assert(BarrierPhase(fc_build_barrier) == PHJ_BUILD_HASHING_OUTER ||
						   BarrierPhase(fc_build_barrier) == PHJ_BUILD_RUNNING ||
						   BarrierPhase(fc_build_barrier) == PHJ_BUILD_DONE);
					if (BarrierPhase(fc_build_barrier) == PHJ_BUILD_HASHING_OUTER)
					{
						/*
						 * 如果是多批处理，我们需要提前对外部关系进行哈希。
						 */
						if (hashtable->nbatch > 1)
							fc_ExecParallelHashJoinPartitionOuter(fc_node);
						BarrierArriveAndWait(fc_build_barrier,
											 WAIT_EVENT_HASH_BUILD_HASH_OUTER);
					}
					else if (BarrierPhase(fc_build_barrier) == PHJ_BUILD_DONE)
					{
						/*
						 * 如果我们附加得太晚，导致作业完成且批处理状态已被释放，我们可以立即返回。
						 */
						return NULL;
					}

					/* 每个后端现在应该选择一个批次进行工作。 */
					Assert(BarrierPhase(fc_build_barrier) == PHJ_BUILD_RUNNING);
					hashtable->curbatch = -1;
					fc_node->hj_JoinState = HJ_NEED_NEW_BATCH;

					continue;
				}
				else
					fc_node->hj_JoinState = HJ_NEED_NEW_OUTER;

				/* FALL THRU */

			case HJ_NEED_NEW_OUTER:

				/*
				 * 我们没有外部元组，尝试获取下一个
				 */
				if (fc_parallel)
					fc_outerTupleSlot =
						fc_ExecParallelHashJoinOuterGetTuple(fc_outerNode, fc_node,
														  &fc_hashvalue);
				else
					fc_outerTupleSlot =
						fc_ExecHashJoinOuterGetTuple(fc_outerNode, fc_node, &fc_hashvalue);

				if (TupIsNull(fc_outerTupleSlot))
				{
					/* 批次结束，或者可能是整个连接 */
					if (HJ_FILL_INNER(fc_node))
					{
						/* 设置扫描以查找未匹配的内部元组 */
						ExecPrepHashTableForUnmatched(fc_node);
						fc_node->hj_JoinState = HJ_FILL_INNER_TUPLES;
					}
					else
						fc_node->hj_JoinState = HJ_NEED_NEW_BATCH;
					continue;
				}

				fc_econtext->ecxt_outertuple = fc_outerTupleSlot;
				fc_node->hj_MatchedOuter = false;

				/*
				 * 在主哈希表或偏斜哈希表中找到此元组的相应桶。
				 */
				fc_node->hj_CurHashValue = fc_hashvalue;
				ExecHashGetBucketAndBatch(hashtable, fc_hashvalue,
										  &fc_node->hj_CurBucketNo, &fc_batchno);
				fc_node->hj_CurSkewBucketNo = ExecHashGetSkewBucket(hashtable,
																 fc_hashvalue);
				fc_node->hj_CurTuple = NULL;

				/*
				 * 此元组可能不属于当前批次（“当前批次”包括偏斜桶，如果有的话）。
				 */
				if (fc_batchno != hashtable->curbatch &&
					fc_node->hj_CurSkewBucketNo == INVALID_SKEW_BUCKET_NO)
				{
					bool		fc_shouldFree;
					MinimalTuple fc_mintuple = ExecFetchSlotMinimalTuple(fc_outerTupleSlot,
																	  &fc_shouldFree);

					/*
					 * 需要将这个外部元组推迟到后面的批次。
					 * 将其保存在相应的外部批次文件中。
					 */
					Assert(fc_parallel_state == NULL);
					Assert(fc_batchno > hashtable->curbatch);
					ExecHashJoinSaveTuple(fc_mintuple, fc_hashvalue,
										  &hashtable->outerBatchFile[fc_batchno]);

					if (fc_shouldFree)
						heap_free_minimal_tuple(fc_mintuple);

					/* 循环，保持在 HJ_NEED_NEW_OUTER 状态 */
					continue;
				}

				/* 好吧，让我们扫描桶以查找匹配项 */
				fc_node->hj_JoinState = HJ_SCAN_BUCKET;

				/* FALL THRU */

			case HJ_SCAN_BUCKET:

				/*
				 * 扫描选择的哈希桶以查找与当前外部的匹配
				 */
				if (fc_parallel)
				{
					if (!ExecParallelScanHashBucket(fc_node, fc_econtext))
					{
						/* 没有匹配项；检查可能的外部连接填充 */
						fc_node->hj_JoinState = HJ_FILL_OUTER_TUPLE;
						continue;
					}
				}
				else
				{
					if (!ExecScanHashBucket(fc_node, fc_econtext))
					{
						/* 没有匹配项；检查可能的外部连接填充 */
						fc_node->hj_JoinState = HJ_FILL_OUTER_TUPLE;
						continue;
					}
				}

				/*
				 * 我们找到了匹配项，但仍需测试非哈希条件。
				 * ExecScanHashBucket 已经设置了调用 ExecQual 所需的所有状态。
				 *
				 * 如果我们通过条件，则保存下次调用的状态，
				 * 并让 ExecProject 形成投影，将其存储在元组表中，
				 * 并返回槽。
				 *
				 * 只有连接条件确定元组匹配状态，但所有条件必须通过才能实际返回元组。
				 */
				if (fc_joinqual == NULL || ExecQual(fc_joinqual, fc_econtext))
				{
					fc_node->hj_MatchedOuter = true;

					if (fc_parallel)
					{
						/*
						 * 目前不支持全连接/右连接的并行连接，
						 * 所以我们不需要设置匹配位。
						 * 实验表明，避免在大型系统上的共享内存流量是值得的。
						 */
						Assert(!HJ_FILL_INNER(fc_node));
					}
					else
					{
						/*
						 * 这实际上仅在 HJ_FILL_INNER(node) 时需要，
						 * 但我们将避免分支并始终设置它。
						 */
						HeapTupleHeaderSetMatch(HJTUPLE_MINTUPLE(fc_node->hj_CurTuple));
					}

					/* 在反连接中，我们从不返回匹配的元组 */
					if (fc_node->js.jointype == JOIN_ANTI)
					{
						fc_node->hj_JoinState = HJ_NEED_NEW_OUTER;
						continue;
					}

					/*
					 * 如果我们只需要连接到第一个匹配的内部元组，
					 * 那么考虑返回这个，但之后继续下一个外部元组。
					 */
					if (fc_node->js.single_match)
						fc_node->hj_JoinState = HJ_NEED_NEW_OUTER;

					if (fc_otherqual == NULL || ExecQual(fc_otherqual, fc_econtext))
						return ExecProject(fc_node->js.ps.ps_ProjInfo);
					else
						InstrCountFiltered2(fc_node, 1);
				}
				else
					InstrCountFiltered1(fc_node, 1);
				break;

			case HJ_FILL_OUTER_TUPLE:

				/*
				 * 当前的外部元组已经没有匹配项了，因此检查
				 * 是否发出虚拟的外部连接元组。不管我们是否发出，
				 * 下一个状态是 NEED_NEW_OUTER。
				 */
				fc_node->hj_JoinState = HJ_NEED_NEW_OUTER;

				if (!fc_node->hj_MatchedOuter &&
					HJ_FILL_OUTER(fc_node))
				{
					/*
					 * 生成一个虚假的连接元组，对于内部元组使用空值，
					 * 如果它通过非连接条件，则返回它。
					 */
					fc_econtext->ecxt_innertuple = fc_node->hj_NullInnerTupleSlot;

					if (fc_otherqual == NULL || ExecQual(fc_otherqual, fc_econtext))
						return ExecProject(fc_node->js.ps.ps_ProjInfo);
					else
						InstrCountFiltered2(fc_node, 1);
				}
				break;

			case HJ_FILL_INNER_TUPLES:

				/*
				 * 我们已经完成一个批次，但我们正在进行右/全连接，
				 * 所以在继续到下一个批次之前，必须发出哈希表中的任何未匹配的内部元组。
				 */
				if (!ExecScanHashTableForUnmatched(fc_node, fc_econtext))
				{
					/* 没有更多未匹配的元组 */
					fc_node->hj_JoinState = HJ_NEED_NEW_BATCH;
					continue;
				}

				/*
				 * 生成一个虚假的连接元组，对于外部元组使用空值，
				 * 如果它通过非连接条件，则返回它。
				 */
				fc_econtext->ecxt_outertuple = fc_node->hj_NullOuterTupleSlot;

				if (fc_otherqual == NULL || ExecQual(fc_otherqual, fc_econtext))
					return ExecProject(fc_node->js.ps.ps_ProjInfo);
				else
					InstrCountFiltered2(fc_node, 1);
				break;

			case HJ_NEED_NEW_BATCH:

				/*
				 * 尝试提前进入下一个批次。如果没有更多则完成。
				 */
				if (fc_parallel)
				{
					if (!fc_ExecParallelHashJoinNewBatch(fc_node))
						return NULL;	/* 并行感知连接结束 */
				}
				else
				{
					if (!fc_ExecHashJoinNewBatch(fc_node))
						return NULL;	/* 并行无意识连接结束 */
				}
				fc_node->hj_JoinState = HJ_NEED_NEW_OUTER;
				break;

			default:
				elog(ERROR, "unrecognized hashjoin state: %d",
					 (int) fc_node->hj_JoinState);
		}
	}
}

/* ----------------------------------------------------------------
 *		ExecHashJoin
 *
 *		并行无意识版本。
 * ----------------------------------------------------------------
 */
static TupleTableSlot *			/* 返回：一个元组或 NULL */
fc_ExecHashJoin(PlanState *fc_pstate)
{
	/*
	 * 在足够智能的编译器上，这应该内联删除
	 * 并行无意识的分支。
	 */
	return fc_ExecHashJoinImpl(fc_pstate, false);
}

/* ----------------------------------------------------------------
 *		ExecParallelHashJoin
 *
 *		并行感知版本。
 * ----------------------------------------------------------------
 */
static TupleTableSlot *			/* 返回：一个元组或 NULL */
fc_ExecParallelHashJoin(PlanState *fc_pstate)
{
	/*
	 * 在足够智能的编译器上，这应该内联删除
	 * 并行无意识的分支。
	 */
	return fc_ExecHashJoinImpl(fc_pstate, true);
}

/* ----------------------------------------------------------------
 *		ExecInitHashJoin
 *
 *		HashJoin 节点的初始化例程。
 * ----------------------------------------------------------------
 */
HashJoinState *
ExecInitHashJoin(HashJoin *fc_node, EState *fc_estate, int fc_eflags)
{
	HashJoinState *fc_hjstate;
	Plan	   *fc_outerNode;
	Hash	   *fc_hashNode;
	TupleDesc	fc_outerDesc,
				fc_innerDesc;
	const TupleTableSlotOps *fc_ops;

	/* 检查不支持的标志 */
	Assert(!(fc_eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));

	/*
	 * 创建状态结构
	 */
	fc_hjstate = makeNode(HashJoinState);
	fc_hjstate->js.ps.plan = (Plan *) fc_node;
	fc_hjstate->js.ps.state = fc_estate;

	/*
	 * 查看 ExecHashJoinInitializeDSM() 和 ExecHashJoinInitializeWorker()
	 * 在这里，如果我们成功启动了并行查询，此函数可能会被替换为并行版本。
	 */
	fc_hjstate->js.ps.ExecProcNode = fc_ExecHashJoin;
	fc_hjstate->js.jointype = fc_node->join.jointype;

	/*
	 * 杂项初始化
	 *
	 * 为节点创建表达式上下文
	 */
	ExecAssignExprContext(fc_estate, &fc_hjstate->js.ps);

	/*
	 * 初始化子节点
	 *
	 * 注意：我们可以抑制内输入的 REWIND 标志，这就相当于打赌哈希将是一个单个批次。
	 * 目前尚不清楚这样做是否会有利。
	 */
	fc_outerNode = outerPlan(fc_node);
	fc_hashNode = (Hash *) innerPlan(fc_node);

	outerPlanState(fc_hjstate) = ExecInitNode(fc_outerNode, fc_estate, fc_eflags);
	fc_outerDesc = ExecGetResultType(outerPlanState(fc_hjstate));
	innerPlanState(fc_hjstate) = ExecInitNode((Plan *) fc_hashNode, fc_estate, fc_eflags);
	fc_innerDesc = ExecGetResultType(innerPlanState(fc_hjstate));

	/*
	 * 初始化结果槽、类型和投影。
	 */
	ExecInitResultTupleSlotTL(&fc_hjstate->js.ps, &TTSOpsVirtual);
	ExecAssignProjectionInfo(&fc_hjstate->js.ps, NULL);

	/*
	 * 元组表初始化
	 */
	fc_ops = ExecGetResultSlotOps(outerPlanState(fc_hjstate), NULL);
	fc_hjstate->hj_OuterTupleSlot = ExecInitExtraTupleSlot(fc_estate, fc_outerDesc,
														fc_ops);

	/*
	 * 检测是否只需考虑第一个匹配的内层元组
	 */
	fc_hjstate->js.single_match = (fc_node->join.inner_unique ||
								fc_node->join.jointype == JOIN_SEMI);

	/* 如有需要，设置外连接的空元组 */
	switch (fc_node->join.jointype)
	{
		case JOIN_INNER:
		case JOIN_SEMI:
			break;
		case JOIN_LEFT:
		case JOIN_ANTI:
			fc_hjstate->hj_NullInnerTupleSlot =
				ExecInitNullTupleSlot(fc_estate, fc_innerDesc, &TTSOpsVirtual);
			break;
		case JOIN_RIGHT:
			fc_hjstate->hj_NullOuterTupleSlot =
				ExecInitNullTupleSlot(fc_estate, fc_outerDesc, &TTSOpsVirtual);
			break;
		case JOIN_FULL:
			fc_hjstate->hj_NullOuterTupleSlot =
				ExecInitNullTupleSlot(fc_estate, fc_outerDesc, &TTSOpsVirtual);
			fc_hjstate->hj_NullInnerTupleSlot =
				ExecInitNullTupleSlot(fc_estate, fc_innerDesc, &TTSOpsVirtual);
			break;
		default:
			elog(ERROR, "unrecognized join type: %d",
				 (int) fc_node->join.jointype);
	}

	/*
	 * 现在进行一些魔法。我们的临时元组槽实际上是 Hash 节点的结果
	 * 元组槽（也就是我们的内层计划）。我们之所以能这样做
	 * 是因为 Hash 节点并不通过 ExecProcNode() 返回元组 -- 相反，
	 * 哈希连接节点使用 ExecScanHashBucket() 获取哈希表的内容。 -cim 6/9/91
	 */
	{
		HashState  *fc_hashstate = (HashState *) innerPlanState(fc_hjstate);
		TupleTableSlot *fc_slot = fc_hashstate->ps.ps_ResultTupleSlot;

		fc_hjstate->hj_HashTupleSlot = fc_slot;
	}

	/*
	 * 初始化子表达式
	 */
	fc_hjstate->js.ps.qual =
		ExecInitQual(fc_node->join.plan.qual, (PlanState *) fc_hjstate);
	fc_hjstate->js.joinqual =
		ExecInitQual(fc_node->join.joinqual, (PlanState *) fc_hjstate);
	fc_hjstate->hashclauses =
		ExecInitQual(fc_node->hashclauses, (PlanState *) fc_hjstate);

	/*
	 * 初始化哈希特定信息
	 */
	fc_hjstate->hj_HashTable = NULL;
	fc_hjstate->hj_FirstOuterTupleSlot = NULL;

	fc_hjstate->hj_CurHashValue = 0;
	fc_hjstate->hj_CurBucketNo = 0;
	fc_hjstate->hj_CurSkewBucketNo = INVALID_SKEW_BUCKET_NO;
	fc_hjstate->hj_CurTuple = NULL;

	fc_hjstate->hj_OuterHashKeys = ExecInitExprList(fc_node->hashkeys,
												 (PlanState *) fc_hjstate);
	fc_hjstate->hj_HashOperators = fc_node->hashoperators;
	fc_hjstate->hj_Collations = fc_node->hashcollations;

	fc_hjstate->hj_JoinState = HJ_BUILD_HASHTABLE;
	fc_hjstate->hj_MatchedOuter = false;
	fc_hjstate->hj_OuterNotEmpty = false;

	return fc_hjstate;
}

/* ----------------------------------------------------------------
 *		ExecEndHashJoin
 *
 *		HashJoin 节点的清理例程
 * ----------------------------------------------------------------
 */
void ExecEndHashJoin(HashJoinState *fc_node)
{
	/*
	 * 释放哈希表
	 */
	if (fc_node->hj_HashTable)
	{
		ExecHashTableDestroy(fc_node->hj_HashTable);
		fc_node->hj_HashTable = NULL;
	}

	/*
	 * 释放表达式上下文
	 */
	ExecFreeExprContext(&fc_node->js.ps);

	/*
	 * 清空元组表
	 */
	ExecClearTuple(fc_node->js.ps.ps_ResultTupleSlot);
	ExecClearTuple(fc_node->hj_OuterTupleSlot);
	ExecClearTuple(fc_node->hj_HashTupleSlot);

	/*
	 * 清理子树
	 */
	ExecEndNode(outerPlanState(fc_node));
	ExecEndNode(innerPlanState(fc_node));
}

/*
 * ExecHashJoinOuterGetTuple
 *
 *		获取并行无关哈希连接的下一个外部元组：要么通过
 *		在第一次遍历中执行外部计划节点，或从临时
 *		哈希连接批次的文件中获取。
 *
 * 如果没有更多的外部元组（在当前批次内），则返回一个空槽。
 *
 * 成功时，元组的哈希值存储在 *hashvalue --- 这是
 * 最初计算得到，或从临时文件中重新读取。
 */
static TupleTableSlot * fc_ExecHashJoinOuterGetTuple(PlanState *fc_outerNode,
						  HashJoinState *fc_hjstate,
						  uint32 *fc_hashvalue)
{
	HashJoinTable hashtable = fc_hjstate->hj_HashTable;
	int			fc_curbatch = hashtable->curbatch;
	TupleTableSlot *fc_slot;

	if (fc_curbatch == 0)			/* 如果是第一次遍历 */
	{
		/*
		 * 检查第一次外部元组是否已经通过
		 * ExecHashJoin() 获取且尚未使用。
		 */
		fc_slot = fc_hjstate->hj_FirstOuterTupleSlot;
		if (!TupIsNull(fc_slot))
			fc_hjstate->hj_FirstOuterTupleSlot = NULL;
		else
			fc_slot = ExecProcNode(fc_outerNode);

		while (!TupIsNull(fc_slot))
		{
			/*
			 * 我们必须计算元组的哈希值。
			 */
			ExprContext *fc_econtext = fc_hjstate->js.ps.ps_ExprContext;

			fc_econtext->ecxt_outertuple = fc_slot;
			if (ExecHashGetHashValue(hashtable, fc_econtext,
									 fc_hjstate->hj_OuterHashKeys,
									 true,	/* 外部元组 */
									 HJ_FILL_OUTER(fc_hjstate),
									 fc_hashvalue))
			{
				/* 记住外部关系在可能的重新扫描中非空 */
				fc_hjstate->hj_OuterNotEmpty = true;

				return fc_slot;
			}

			/*
			 * 该元组因为 NULL 无法匹配，因此丢弃它并
			 * 继续下一个元组。
			 */
			fc_slot = ExecProcNode(fc_outerNode);
		}
	}
	else if (fc_curbatch < hashtable->nbatch)
	{
		BufFile    *fc_file = hashtable->outerBatchFile[fc_curbatch];

		/*
		 * 在外连接的情况下，即使批次文件
		 * 是空的，我们也可能会到达这里。
		 */
		if (fc_file == NULL)
			return NULL;

		fc_slot = fc_ExecHashJoinGetSavedTuple(fc_hjstate,
										 fc_file,
										 fc_hashvalue,
										 fc_hjstate->hj_OuterTupleSlot);
		if (!TupIsNull(fc_slot))
			return fc_slot;
	}

	/* 本批次结束 */
	return NULL;
}

/*
 * ExecHashJoinOuterGetTuple 的并行变体。
 */
static TupleTableSlot * fc_ExecParallelHashJoinOuterGetTuple(PlanState *fc_outerNode,
								  HashJoinState *fc_hjstate,
								  uint32 *fc_hashvalue)
{
	HashJoinTable hashtable = fc_hjstate->hj_HashTable;
	int			fc_curbatch = hashtable->curbatch;
	TupleTableSlot *fc_slot;

	/*
	 * 在并行哈希情况下，我们仅直接运行外部计划
	 * 用于单批哈希连接。否则即使是批次 0，
	 * 我们也必须访问批次文件。
	 */
	if (fc_curbatch == 0 && hashtable->nbatch == 1)
	{
		fc_slot = ExecProcNode(fc_outerNode);

		while (!TupIsNull(fc_slot))
		{
			ExprContext *fc_econtext = fc_hjstate->js.ps.ps_ExprContext;

			fc_econtext->ecxt_outertuple = fc_slot;
			if (ExecHashGetHashValue(hashtable, fc_econtext,
									 fc_hjstate->hj_OuterHashKeys,
									 true,	/* 外部元组 */
									 HJ_FILL_OUTER(fc_hjstate),
									 fc_hashvalue))
				return fc_slot;

			/*
			 * 该元组因为 NULL 无法匹配，因此丢弃它并
			 * 继续下一个元组。
			 */
			fc_slot = ExecProcNode(fc_outerNode);
		}
	}
	else if (fc_curbatch < hashtable->nbatch)
	{
		MinimalTuple fc_tuple;

		fc_tuple = sts_parallel_scan_next(hashtable->batches[fc_curbatch].outer_tuples,
									   fc_hashvalue);
		if (fc_tuple != NULL)
		{
			ExecForceStoreMinimalTuple(fc_tuple,
									   fc_hjstate->hj_OuterTupleSlot,
									   false);
			fc_slot = fc_hjstate->hj_OuterTupleSlot;
			return fc_slot;
		}
		else
			ExecClearTuple(fc_hjstate->hj_OuterTupleSlot);
	}

	/* 本批次结束 */
	return NULL;
}

/*
 * ExecHashJoinNewBatch
 *		切换到新的哈希连接批次
 *
 * 如果成功则返回 true，如果没有更多批次则返回 false。
 */
static bool fc_ExecHashJoinNewBatch(HashJoinState *fc_hjstate)
{
	HashJoinTable hashtable = fc_hjstate->hj_HashTable;
	int			fc_nbatch;
	int			fc_curbatch;
	BufFile    *fc_innerFile;
	TupleTableSlot *fc_slot;
	uint32		fc_hashvalue;

	fc_nbatch = hashtable->nbatch;
	fc_curbatch = hashtable->curbatch;

	if (fc_curbatch > 0)
	{
		/*
		 * 我们不再需要之前的外部批次文件；立即关闭它
		 * 以释放磁盘空间。
		 */
		if (hashtable->outerBatchFile[fc_curbatch])
			BufFileClose(hashtable->outerBatchFile[fc_curbatch]);
		hashtable->outerBatchFile[fc_curbatch] = NULL;
	}
	else						/* 我们刚刚完成了第一个批次 */
	{
		/*
		 * 重置一些偏斜优化状态变量，因为在第一次批次后我们
		 * 不再需要考虑偏斜元组。我们即将进行的内存上下文重置
		 * 将释放偏斜哈希表本身。
		 */
		hashtable->skewEnabled = false;
		hashtable->skewBucket = NULL;
		hashtable->skewBucketNums = NULL;
		hashtable->nSkewBuckets = 0;
		hashtable->spaceUsedSkew = 0;
	}

	/*
	 * 我们总是可以跳过任何在两侧完全空的批次。
	 * 我们有时可以跳过只在一侧空的批次，但有例外：
	 *
	 * 1. 在左/全外连接中，即使内批次为空，我们也必须处理外部批次。
	 * 类似地，在右/全外连接中，即使外部批次为空，我们也必须处理内批次。
	 *
	 * 2. 如果我们自初始估计以来增加了 nbatch，我们必须
	 * 扫描内批次，因为它们可能包含需要重新分配到后续内批次的元组。
	 *
	 * 3. 同样，如果我们自开始外扫描以来增加了 nbatch，我们需要
	 * 重新扫描外批次，以防它们包含需要重新分配的元组。
	 */
	fc_curbatch++;
	while (fc_curbatch < fc_nbatch &&
		   (hashtable->outerBatchFile[fc_curbatch] == NULL ||
			hashtable->innerBatchFile[fc_curbatch] == NULL))
	{
		if (hashtable->outerBatchFile[fc_curbatch] &&
			HJ_FILL_OUTER(fc_hjstate))
			break;				/* 必须处理，因规则 1 */
		if (hashtable->innerBatchFile[fc_curbatch] &&
			HJ_FILL_INNER(fc_hjstate))
			break;				/* 必须处理，因规则 1 */
		if (hashtable->innerBatchFile[fc_curbatch] &&
			fc_nbatch != hashtable->nbatch_original)
			break;				/* 必须处理，因规则 2 */
		if (hashtable->outerBatchFile[fc_curbatch] &&
			fc_nbatch != hashtable->nbatch_outstart)
			break;				/* 必须处理，因规则 3 */
		/* 我们可以忽略这个批次。 */
		/* 立即释放相关的临时文件。 */
		if (hashtable->innerBatchFile[fc_curbatch])
			BufFileClose(hashtable->innerBatchFile[fc_curbatch]);
		hashtable->innerBatchFile[fc_curbatch] = NULL;
		if (hashtable->outerBatchFile[fc_curbatch])
			BufFileClose(hashtable->outerBatchFile[fc_curbatch]);
		hashtable->outerBatchFile[fc_curbatch] = NULL;
		fc_curbatch++;
	}

	if (fc_curbatch >= fc_nbatch)
		return false;			/* 不再有批次 */

	hashtable->curbatch = fc_curbatch;

	/*
	 * 使用新的内部批次重新加载哈希表（可能是空的）
	 */
	ExecHashTableReset(hashtable);

	fc_innerFile = hashtable->innerBatchFile[fc_curbatch];

	if (fc_innerFile != NULL)
	{
		if (BufFileSeek(fc_innerFile, 0, 0L, SEEK_SET))
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not rewind hash-join temporary file")));

		while ((fc_slot = fc_ExecHashJoinGetSavedTuple(fc_hjstate,
												 fc_innerFile,
												 &fc_hashvalue,
												 fc_hjstate->hj_HashTupleSlot)))
		{
			/*
			 * 注意：某些元组可能会被发送到未来的批次中。同时，
			 * hashtable->nbatch 在这里可能会增加！
			 */
			ExecHashTableInsert(hashtable, fc_slot, fc_hashvalue);
		}

		/*
		 * 在我们构建哈希表之后，内部批次文件不再需要。
		 */
		BufFileClose(fc_innerFile);
		hashtable->innerBatchFile[fc_curbatch] = NULL;
	}

	/*
	 * 重新卷起外部批次文件（如果存在），以便我们可以开始读取它。
	 */
	if (hashtable->outerBatchFile[fc_curbatch] != NULL)
	{
		if (BufFileSeek(hashtable->outerBatchFile[fc_curbatch], 0, 0L, SEEK_SET))
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not rewind hash-join temporary file")));
	}

	return true;
}

/*
 * 选择一个批次进行工作，并附加到它。如果成功返回真，
 * 如果没有更多批次返回假。
 */
static bool fc_ExecParallelHashJoinNewBatch(HashJoinState *fc_hjstate)
{
	HashJoinTable hashtable = fc_hjstate->hj_HashTable;
	int			fc_start_batchno;
	int			fc_batchno;

	/*
	 * 如果我们已经附加到一个批次，记得不要再检查
	 * 它，并从中分离（如果我们是最后一个分离，可能会释放哈希表）。
	 */
	if (hashtable->curbatch >= 0)
	{
		hashtable->batches[hashtable->curbatch].done = true;
		ExecHashTableDetachBatch(hashtable);
	}

	/*
	 * 搜索一个未完成的批次。当参与者比批次多时，我们使用
	 * 原子计数器从不同的批次开始搜索。
	 */
	fc_batchno = fc_start_batchno =
		pg_atomic_fetch_add_u32(&hashtable->parallel_state->distributor, 1) %
		hashtable->nbatch;
	do
	{
		uint32		fc_hashvalue;
		MinimalTuple fc_tuple;
		TupleTableSlot *fc_slot;

		if (!hashtable->batches[fc_batchno].done)
		{
			SharedTuplestoreAccessor *fc_inner_tuples;
			Barrier    *fc_batch_barrier =
			&hashtable->batches[fc_batchno].shared->batch_barrier;

			switch (BarrierAttach(fc_batch_barrier))
			{
				case PHJ_BATCH_ELECTING:

					/* 一个后端分配哈希表。 */
					if (BarrierArriveAndWait(fc_batch_barrier,
											 WAIT_EVENT_HASH_BATCH_ELECT))
						ExecParallelHashTableAlloc(hashtable, fc_batchno);
					/* Fall through. */

				case PHJ_BATCH_ALLOCATING:
					/* 等待分配完成。 */
					BarrierArriveAndWait(fc_batch_barrier,
										 WAIT_EVENT_HASH_BATCH_ALLOCATE);
					/* Fall through. */

				case PHJ_BATCH_LOADING:
					/* 开始（或加入）加载元组。 */
					ExecParallelHashTableSetCurrentBatch(hashtable, fc_batchno);
					fc_inner_tuples = hashtable->batches[fc_batchno].inner_tuples;
					sts_begin_parallel_scan(fc_inner_tuples);
					while ((fc_tuple = sts_parallel_scan_next(fc_inner_tuples,
														   &fc_hashvalue)))
					{
						ExecForceStoreMinimalTuple(fc_tuple,
												   fc_hjstate->hj_HashTupleSlot,
												   false);
						fc_slot = fc_hjstate->hj_HashTupleSlot;
						ExecParallelHashTableInsertCurrentBatch(hashtable, fc_slot,
																fc_hashvalue);
					}
					sts_end_parallel_scan(fc_inner_tuples);
					BarrierArriveAndWait(fc_batch_barrier,
										 WAIT_EVENT_HASH_BATCH_LOAD);
					/* Fall through. */

				case PHJ_BATCH_PROBING:

					/*
					 * 这个批次已经准备好进行探测。返回控制给
					 * 调用者。我们保持附加到 batch_barrier，这样哈希表
					 * 会在每个人完成探测之前保持活动状态，但不允许
					 * 任何参与者在这个屏障上再次等待（否则可能会发生死锁）。
					 * 所有附加的参与者最终必须调用
					 * BarrierArriveAndDetach()，以便达到最终阶段
					 * PHJ_BATCH_DONE。
					 */
					ExecParallelHashTableSetCurrentBatch(hashtable, fc_batchno);
					sts_begin_parallel_scan(hashtable->batches[fc_batchno].outer_tuples);
					return true;

				case PHJ_BATCH_DONE:

					/*
					 * 已经完成。分离并重新开始（如果还有剩余）。
					 */
					BarrierDetach(fc_batch_barrier);
					hashtable->batches[fc_batchno].done = true;
					hashtable->curbatch = -1;
					break;

				default:
					elog(ERROR, "unexpected batch phase %d",
						 BarrierPhase(fc_batch_barrier));
			}
		}
		fc_batchno = (fc_batchno + 1) % hashtable->nbatch;
	} while (fc_batchno != fc_start_batchno);

	return false;
}

/*
 * ExecHashJoinSaveTuple
 *		将一个元组保存到批次文件中。
 *
 * 为每个元组记录在文件中的数据是它的哈希值，
 * 然后是以 MinimalTuple 格式的元组。
 *
 * 注意：务必在常规执行器上下文中调用此函数，
 * 而不是在短暂上下文中；否则临时文件缓冲区
 * 将被搞乱。
 */
void ExecHashJoinSaveTuple(MinimalTuple fc_tuple, uint32 fc_hashvalue,
					  BufFile **fc_fileptr)
{
	BufFile    *fc_file = *fc_fileptr;

	if (fc_file == NULL)
	{
		/* 首先写入此批次文件，因此打开它。 */
		fc_file = BufFileCreateTemp(false);
		*fc_fileptr = fc_file;
	}

	BufFileWrite(fc_file, (void *) &fc_hashvalue, sizeof(uint32));
	BufFileWrite(fc_file, (void *) fc_tuple, fc_tuple->t_len);
}

/*
 * ExecHashJoinGetSavedTuple
 *		从批次文件中读取下一个元组。如果没有更多，则返回 NULL。
 *
 * 如果成功，*hashvalue 被设置为元组的哈希值，元组
 * 本身存储在给定的槽中。
 */
static TupleTableSlot * fc_ExecHashJoinGetSavedTuple(HashJoinState *fc_hjstate,
						  BufFile *fc_file,
						  uint32 *fc_hashvalue,
						  TupleTableSlot *fc_tupleSlot)
{
	uint32		fc_header[2];
	size_t		fc_nread;
	MinimalTuple fc_tuple;

	/*
	 * 我们在这里检查中断，因为这通常被视为一种替代
	 * ExecProcNode() 调用的代码路径，其中将包含
	 * 这样的检查。
	 */
	CHECK_FOR_INTERRUPTS();

	/*
	 * 由于哈希值和 MinimalTuple 长度字都是 uint32，
	 * 我们可以在一个 BufFileRead() 调用中读取它们，而不需要
	 * 任何类型的欺骗。
	 */
	fc_nread = BufFileRead(fc_file, (void *) fc_header, sizeof(fc_header));
	if (fc_nread == 0)				/* 文件结束 */
	{
		ExecClearTuple(fc_tupleSlot);
		return NULL;
	}
	if (fc_nread != sizeof(fc_header))
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not read from hash-join temporary file: read only %zu of %zu bytes",
						fc_nread, sizeof(fc_header))));
	*fc_hashvalue = fc_header[0];
	fc_tuple = (MinimalTuple) palloc(fc_header[1]);
	fc_tuple->t_len = fc_header[1];
	fc_nread = BufFileRead(fc_file,
						(void *) ((char *) fc_tuple + sizeof(uint32)),
						fc_header[1] - sizeof(uint32));
	if (fc_nread != fc_header[1] - sizeof(uint32))
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not read from hash-join temporary file: read only %zu of %zu bytes",
						fc_nread, fc_header[1] - sizeof(uint32))));
	ExecForceStoreMinimalTuple(fc_tuple, fc_tupleSlot, true);
	return fc_tupleSlot;
}


void ExecReScanHashJoin(HashJoinState *fc_node)
{
	/*
	 * 在多批次连接中，我们目前必须以困难的方式进行重新扫描，
	 * 主要是因为批次临时文件可能已经被释放。但是
	 * 如果是单批次连接，并且内部子节点没有参数变化，
	 * 那么我们可以直接重用现有的哈希表，而不必重建它。
	 */
	if (fc_node->hj_HashTable != NULL)
	{
		if (fc_node->hj_HashTable->nbatch == 1 &&
			fc_node->js.ps.righttree->chgParam == NULL)
		{
			/*
			 * 可以重用哈希表；也不需要重新扫描内部。
			 *
			 * 然而，如果是右连接/全连接，我们最好重置
			 * 表中包含的内部元组匹配标志。
			 */
			if (HJ_FILL_INNER(fc_node))
				ExecHashTableResetMatchFlags(fc_node->hj_HashTable);

			/*
			 * 此外，我们需要重置有关外部关系的空状态，
			 * 以便外部的新扫描可以正确更新它，如果这次
			 * 结果显示为空。（现在清除它没有害处，因为
			 * ExecHashJoin 不需要这些信息。在其他情况下，
			 * 哈希表不存在或我们正在销毁它时，保持
			 * 这个状态不变，因为 ExecHashJoin 在第一次
			 * 运行时会需要它。）
			 */
			fc_node->hj_OuterNotEmpty = false;

			/* ExecHashJoin 可以跳过 BUILD_HASHTABLE 步骤 */
			fc_node->hj_JoinState = HJ_NEED_NEW_OUTER;
		}
		else
		{
			/* 必须销毁并重建哈希表 */
			HashState  *fc_hashNode = castNode(HashState, innerPlanState(fc_node));

			Assert(fc_hashNode->hashtable == fc_node->hj_HashTable);
			/* 如果需要，从旧哈希表中累积统计信息 */
			/* （这应与 ExecShutdownHash 匹配） */
			if (fc_hashNode->ps.instrument && !fc_hashNode->hinstrument)
				fc_hashNode->hinstrument = (HashInstrumentation *)
					palloc0(sizeof(HashInstrumentation));
			if (fc_hashNode->hinstrument)
				ExecHashAccumInstrumentation(fc_hashNode->hinstrument,
											 fc_hashNode->hashtable);
			/* 为安全起见，确保也清除子计划节点的指针 */
			fc_hashNode->hashtable = NULL;

			ExecHashTableDestroy(fc_node->hj_HashTable);
			fc_node->hj_HashTable = NULL;
			fc_node->hj_JoinState = HJ_BUILD_HASHTABLE;

			/*
			 * 如果子节点的 chgParam 不为 null，则计划将通过
			 * 第一个 ExecProcNode 进行重新扫描。
			 */
			if (fc_node->js.ps.righttree->chgParam == NULL)
				ExecReScan(fc_node->js.ps.righttree);
		}
	}

	/* 始终重置元组内部状态 */
	fc_node->hj_CurHashValue = 0;
	fc_node->hj_CurBucketNo = 0;
	fc_node->hj_CurSkewBucketNo = INVALID_SKEW_BUCKET_NO;
	fc_node->hj_CurTuple = NULL;

	fc_node->hj_MatchedOuter = false;
	fc_node->hj_FirstOuterTupleSlot = NULL;

	/*
	 * 如果子节点的chgParam不为空，则计划将由第一个ExecProcNode重新扫描。
	 */
	if (fc_node->js.ps.lefttree->chgParam == NULL)
		ExecReScan(fc_node->js.ps.lefttree);
}

void ExecShutdownHashJoin(HashJoinState *fc_node)
{
	if (fc_node->hj_HashTable)
	{
		/*
		 * 在DSM内存消失之前从共享状态分离。这确保在ExecEndHashJoin运行时，我们没有指向DSM内存的任何指针。
		 */
		ExecHashTableDetachBatch(fc_node->hj_HashTable);
		ExecHashTableDetach(fc_node->hj_HashTable);
	}
}

static void fc_ExecParallelHashJoinPartitionOuter(HashJoinState *fc_hjstate)
{
	PlanState  *fc_outerState = outerPlanState(fc_hjstate);
	ExprContext *fc_econtext = fc_hjstate->js.ps.ps_ExprContext;
	HashJoinTable hashtable = fc_hjstate->hj_HashTable;
	TupleTableSlot *fc_slot;
	uint32		fc_hashvalue;
	int			fc_i;

	Assert(fc_hjstate->hj_FirstOuterTupleSlot == NULL);

	/* 执行外部计划，将所有元组写入共享元组存储。 */
	for (;;)
	{
		fc_slot = ExecProcNode(fc_outerState);
		if (TupIsNull(fc_slot))
			break;
		fc_econtext->ecxt_outertuple = fc_slot;
		if (ExecHashGetHashValue(hashtable, fc_econtext,
								 fc_hjstate->hj_OuterHashKeys,
								 true,	/* 外部元组 */
								 HJ_FILL_OUTER(fc_hjstate),
								 &fc_hashvalue))
		{
			int			fc_batchno;
			int			fc_bucketno;
			bool		fc_shouldFree;
			MinimalTuple fc_mintup = ExecFetchSlotMinimalTuple(fc_slot, &fc_shouldFree);

			ExecHashGetBucketAndBatch(hashtable, fc_hashvalue, &fc_bucketno,
									  &fc_batchno);
			sts_puttuple(hashtable->batches[fc_batchno].outer_tuples,
						 &fc_hashvalue, fc_mintup);

			if (fc_shouldFree)
				heap_free_minimal_tuple(fc_mintup);
		}
		CHECK_FOR_INTERRUPTS();
	}

	/* 确保所有外部分区可以被任何后端读取。 */
	for (fc_i = 0; fc_i < hashtable->nbatch; ++fc_i)
		sts_end_write(hashtable->batches[fc_i].outer_tuples);
}

void ExecHashJoinEstimate(HashJoinState *fc_state, ParallelContext *fc_pcxt)
{
	shm_toc_estimate_chunk(&fc_pcxt->estimator, sizeof(ParallelHashJoinState));
	shm_toc_estimate_keys(&fc_pcxt->estimator, 1);
}

void ExecHashJoinInitializeDSM(HashJoinState *fc_state, ParallelContext *fc_pcxt)
{
	int			fc_plan_node_id = fc_state->js.ps.plan->plan_node_id;
	HashState  *fc_hashNode;
	ParallelHashJoinState *fc_pstate;

	/*
	 * 如果我们未能创建一个真实的DSM段，则禁用共享哈希表模式，因为这意味着我们没有DSA区域可以使用。
	 */
	if (fc_pcxt->seg == NULL)
		return;

	ExecSetExecProcNode(&fc_state->js.ps, fc_ExecParallelHashJoin);

	/*
	 * 设置协调对共享哈希表的访问所需的状态，使用计划节点ID作为toc键。
	 */
	fc_pstate = shm_toc_allocate(fc_pcxt->toc, sizeof(ParallelHashJoinState));
	shm_toc_insert(fc_pcxt->toc, fc_plan_node_id, fc_pstate);

	/*
	 * 且初始时没有批次地设置共享哈希连接状态。
	 * ExecHashTableCreate()稍后将准备至少一个并设置nbatch和space_allowed。
	 */
	fc_pstate->nbatch = 0;
	fc_pstate->space_allowed = 0;
	fc_pstate->batches = InvalidDsaPointer;
	fc_pstate->old_batches = InvalidDsaPointer;
	fc_pstate->nbuckets = 0;
	fc_pstate->growth = PHJ_GROWTH_OK;
	fc_pstate->chunk_work_queue = InvalidDsaPointer;
	pg_atomic_init_u32(&fc_pstate->distributor, 0);
	fc_pstate->nparticipants = fc_pcxt->nworkers + 1;
	fc_pstate->total_tuples = 0;
	LWLockInitialize(&fc_pstate->lock,
					 LWTRANCHE_PARALLEL_HASH_JOIN);
	BarrierInit(&fc_pstate->build_barrier, 0);
	BarrierInit(&fc_pstate->grow_batches_barrier, 0);
	BarrierInit(&fc_pstate->grow_buckets_barrier, 0);

	/* 设置我们将用于共享临时文件的空间。 */
	SharedFileSetInit(&fc_pstate->fileset, fc_pcxt->seg);

	/* 初始化哈希节点中的共享状态。 */
	fc_hashNode = (HashState *) innerPlanState(fc_state);
	fc_hashNode->parallel_state = fc_pstate;
}

/* ----------------------------------------------------------------
 *		ExecHashJoinReInitializeDSM
 *
 *		在开始新的扫描之前重置共享状态。
 * ----------------------------------------------------------------
 */
void ExecHashJoinReInitializeDSM(HashJoinState *fc_state, ParallelContext *fc_cxt)
{
	int			fc_plan_node_id = fc_state->js.ps.plan->plan_node_id;
	ParallelHashJoinState *fc_pstate;

	/* 如果我们未能创建DSM段，则无事可做。 */
	if (fc_cxt->seg == NULL)
		return;

	fc_pstate = shm_toc_lookup(fc_cxt->toc, fc_plan_node_id, false);

	/*
	 * 通过重置共享哈希表并将build_barrier快速向前推进到PHJ_BUILD_DONE以及批次0的batch_barrier到PHJ_BATCH_PROBING，对于单批次情况，可以重复使用共享哈希表，
	 * 但是目前共享哈希表已经被最后一个从批次分离的参与者释放了。我们可以考虑在单批次连接中保留它。
	 * 我们还需要调整finalize_plan()，以防止它为并行哈希节点记录虚拟依赖关系，从而阻止重新扫描优化。目前我们不尝试。
	 */

	/* 分离，释放任何剩余的共享内存。 */
	if (fc_state->hj_HashTable != NULL)
	{
		ExecHashTableDetachBatch(fc_state->hj_HashTable);
		ExecHashTableDetach(fc_state->hj_HashTable);
	}

	/* 清除任何共享批次文件。 */
	SharedFileSetDeleteAll(&fc_pstate->fileset);

	/* 将build_barrier重置为PHJ_BUILD_ELECTING以便我们可以再绕一圈。 */
	BarrierInit(&fc_pstate->build_barrier, 0);
}

void ExecHashJoinInitializeWorker(HashJoinState *fc_state,
							 ParallelWorkerContext *fc_pwcxt)
{
	HashState  *fc_hashNode;
	int			fc_plan_node_id = fc_state->js.ps.plan->plan_node_id;
	ParallelHashJoinState *fc_pstate =
	shm_toc_lookup(fc_pwcxt->toc, fc_plan_node_id, false);

	/* 连接到共享临时文件的空间。 */
	SharedFileSetAttach(&fc_pstate->fileset, fc_pwcxt->seg);

	/* 连接到哈希节点中的共享状态。 */
	fc_hashNode = (HashState *) innerPlanState(fc_state);
	fc_hashNode->parallel_state = fc_pstate;

	ExecSetExecProcNode(&fc_state->js.ps, fc_ExecParallelHashJoin);
}
